/**
 * _
 * 
 * @param {Object} obj 
 * 
 * @example const a = { foo: 'bar', obj: { a: 1, b: 2 } };
 *          const b = deepClone(a); // a !== b, a.obj !== b.obj
 */
export const deepClone = obj => {
  let clone = Object.assign({}, obj);
  Object.keys(clone).forEach(
    key => (clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
  );
  return clone;
};

/**
 * _
 * 
 * @param {Object} obj 
 * @param {Object[]} defs 
 * 
 * @example defaults({ a: 1 }, { b: 2 }, { b: 6 }, { a: 3 }); // { a: 1, b: 2 }
 */
export const defaults = (obj, ...defs) => Object.assign({}, obj, ...defs.reverse(), obj);

/**
 * _
 * 
 * @param {Object} a 
 * @param {Object} b
 * 
 * @example equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true 
 */
export const equals = (a, b) => {
  if (a === b) return true;
  if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
  if (!a || !b || (typeof a != 'object' && typeof b !== 'object')) return a === b;
  if (a === null || a === undefined || b === null || b === undefined) return false;
  if (a.prototype !== b.prototype) return false;
  let keys = Object.keys(a);
  if (keys.length !== Object.keys(b).length) return false;
  return keys.every(k => equals(a[k], b[k]));
};

/**
 * _
 * 
 * @param {Object} obj 
 * @param {Function} fn 
 * 
 * @example findKey(
 *            {
 *              barney: { age: 36, active: true },
 *              fred: { age: 40, active: false },
 *              pebbles: { age: 1, active: true }
 *            },
 *            o => o['active']
 *          ); // 'barney'
 */
export const findKey = (obj, fn) => Object.keys(obj).find(key => fn(obj[key], key, obj));

/**
 * _
 * 
 * @param {Object} obj 
 * @param {Function} fn 
 * 
 * findLastKey(
 * {
 *   barney: { age: 36, active: true },
 *   fred: { age: 40, active: false },
 *   pebbles: { age: 1, active: true }
 * },
 * o => o['active']
 * ); // 'pebbles'
 */
const findLastKey = (obj, fn) =>
  Object.keys(obj)
    .reverse()
    .find(key => fn(obj[key], key, obj));

/**
 * flattenObject
 * 
 * @param {Object} obj
 * @param {String} [prefix = ''] 
 * 
 * @example
 * flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 } 
 */  
export const flattenObject = (obj, prefix = '') => {
  return Object.keys(obj).reduce((acc, k) => {
    const pre = prefix.length ? prefix + '.' : '';
    if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
    else acc[pre + k] = obj[k];
    return acc;
  }, {});
}

/**
 * object get
 * 
 * @param {*} from 
 * @param {*} selectors 
 * 
 * @example
 * const obj = { selector: { to: { val: 'val to select' } }, target: [1, 2, { a: 'test' }] };
 * get(obj, 'selector.to.val', 'target[0]', 'target[2].a'); // ['val to select', 1, 'test']
 */
export const get = (from, ...selectors) =>
[...selectors].map(s => {
  return s
          .replace(/\[([^\[\]]*)\]/g, '.$1.')
          .split('.')
          .filter(t => t !== '')
          .reduce((prev, cur) => prev && prev[cur], from)
});

/**
 * _
 * 
 * @param {Object} obj 
 * @param {Function} fn 
 * 
 * @example mapKeys({ a: 1, b: 2 }, (val, key) => key + val); // { a1: 1, b2: 2 }
 */
export const mapKeys = (obj, fn) =>
  Object.keys(obj).reduce((acc, k) => {
    acc[fn(obj[k], k, obj)] = obj[k];
    return acc;
  }, {});


/**
 * _
 * 
 * @param {Object} obj 
 * @param {Object} source 
 * @param {Function} fn 
 * 
 * @example
 * 
 * const isGreeting = val => /^h(?:i|ello)$/.test(val);
 * matchesWith(
 *    { greeting: 'hello' },
 *    { greeting: 'hi' },
 *    (oV, sV) => isGreeting(oV) && isGreeting(sV)
 * ); // true
 */
export const matchesWith = (obj, source, fn) =>
Object.keys(source).every(
  key =>
    obj.hasOwnProperty(key) && fn
      ? fn(obj[key], source[key], key, obj, source)
      : obj[key] == source[key]
);  

/**
 * _
 * 
 * @param {Paris[]} arr 
 * 
 * @example 
 * objectFromPairs([['a', 1], ['b', 2]]); // {a: 1, b: 2}
 */
export const objectFromPairs = arr => arr.reduce((a, v) => ((a[v[0]] = v[1]), a), {});


/**
 * _
 * 
 * @param {Object} obj 
 * @param {Array} arr 
 * 
 * @example omitByKeys({ a: 1, b: '2', c: 3 }, ['b']); // { 'a': 1, 'c': 3 }
 */
export const omitByKeys = (obj, arr) =>
  Object.keys(obj)
    .filter(k => !arr.includes(k))
    .reduce((acc, key) => ((acc[key] = obj[key]), acc), {});

/**
 * _
 * 
 * @param {Object} obj 
 * @param {Function} fn 
 * 
 * @example omitBy({ a: 1, b: '2', c: 3 }, x => typeof x === 'number'); // { b: '2' }
 */    
export const omitBy = (obj, fn) =>
    Object.keys(obj)
      .filter(k => !fn(obj[k], k))
      .reduce((acc, key) => ((acc[key] = obj[key]), acc), {});

/**
 * _
 * 
 * @param {Array} arr 
 * @param {Array} props
 * @param {Array} orders ['asc', 'desc']
 */      
export const orderBy = (arr, props, orders) =>
  [...arr].sort((a, b) =>
    props.reduce((acc, prop, i) => {
      if (acc === 0) {
        const [p1, p2] = orders && orders[i] === 'desc' ? [b[prop], a[prop]] : [a[prop], b[prop]];
        acc = p1 > p2 ? 1 : p1 < p2 ? -1 : 0;
      }
      return acc;
    }, 0)
);

/**
 * _
 * 
 * @param {Object} obj
 * @param {Array} arr  
 * 
 * @example pickByKeys({ a: 1, b: '2', c: 3 }, ['a', 'c']); // { 'a': 1, 'c': 3 }
 */  
export const pickByKeys = (obj, arr) => arr.reduce((acc, curr) => (curr in obj && (acc[curr] = obj[curr]), acc), {});

/**
 * unflattenObject
 * 
 * @param {*} obj 
 * 
 * @example unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 }
 */
export const unflattenObject = obj =>
  Object.keys(obj).reduce((acc, k) => {
    if (k.indexOf('.') !== -1) {
      const keys = k.split('.');
      Object.assign(
        acc,
        JSON.parse(
          '{' +
            keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') +
            obj[k] +
            '}'.repeat(keys.length)
        )
      );
    } else acc[k] = obj[k];
    return acc;
  }, {});



/**
 * pickBy
 * 
 * @param {Object} obj 
 * @param {Function} fn 
 */
export const pickBy = (obj, fn) =>
  Object.keys(obj)
    .filter(k => fn(obj[k], k))
    .reduce((acc, key) => ((acc[key] = obj[key]), acc), {});

/**
 * 深度得到一个object
 * 
 * @param {Object} from 
 * @param {String} path 
 */
export var deepGet = function deepGet(from) {
    for (var _len = arguments.length, selectors = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        selectors[_key - 1] = arguments[_key];
    }

    return [].concat(selectors).map(function (s) {
        return s.replace(/\[([^\[\]]*)\]/g, '.$1.').split('.').filter(function (t) {
            return t !== '';
        }).reduce(function (prev, cur) {
            return prev && prev[cur];
        }, from);
    });
};

/**
 * 深度得到一个object
 * 
 * @param {Object} obj 
 * @param {String} path 
 * @returns {Object | null} 
 */
export const getByPath = function(obj, path) {
    let ret = deepGet(obj, path);
    if (ret) {
        return ret[0];
    }
    return null;
};    